Skip to content

fix(rest): bound buckets and rate limit verified JWT identities - #1389

Open
Erosenin2 wants to merge 2 commits into
CalloraOrg:mainfrom
Erosenin2:fix/1269-rest-rate-limit-identity-eviction
Open

Erosenin2 wants to merge 2 commits into
CalloraOrg:mainfrom
Erosenin2:fix/1269-rest-rate-limit-identity-eviction

Conversation

@Erosenin2

@Erosenin2 Erosenin2 commented Sep 29, 2026 •

Copy link
Copy Markdown

Overview

The REST limiter retained every bucket indefinitely and reused a resolver that can accept forwarded user identities. REST limits now use verified JWT subjects (or legacy signed userId claims), otherwise client IP. Storage has a hard 10,000-bucket default bound with LRU eviction, and an idle cleanup timer removes expired buckets even without further traffic.

Related Issue

Closes #1269

Changes

  • Extract JWT-only resolution from requireAuth.ts, retaining signature, algorithm, expiry and claim validation. Existing authentication keeps its original userId precedence and signed-gateway support; the REST limiter never uses forwarded identities.
  • Prefer a valid JWT sub for REST keys, retaining compatibility with existing signed userId tokens. Invalid, expired, inactive or unverifiable tokens use the IP bucket.
  • Add optional maxBuckets, defaulting to 10,000, and refresh LRU order on checks, including denied requests. peek remains non-consuming.
  • Sweep expired buckets every window using an unref timer; stop when empty. reset and dispose release timers, and later use restarts cleanup. Validate positive integer limits and supported timer intervals.
  • Replace outdated REST header-auth test fixtures with signed JWTs and add identity-spoofing, subject-priority, 100k-key load, LRU, idle-expiry and cleanup lifecycle regressions.

Security and Compatibility

Rotating x-user-id cannot obtain fresh REST buckets, including when signed forwarding is enabled for route authentication. JWT subjects are read only after cryptographic verification. Existing IP extraction, fixed-window quotas, 429 body and Retry-After behavior remain unchanged. No dependencies or billing production logic change.

Capacity eviction can reset a displaced client's quota under saturation, as with the existing bounded gateway store; the limit is per process and does not provide distributed coordination. Recent active/denied clients retain their buckets preferentially. Cleanup costs at most one scan of the bounded map per window, and timers do not keep the process alive. Removed custom limiters should call dispose().

Verification Results

Upstream currently lacks package.json and jest.env-setup.cjs after commit 599ab6e. Local validation used copies from that commit's verified parent, excluded from this PR. Installed the existing lockfile with npm ci --ignore-scripts; no dependencies changed.

Command Observed result
npm test -- src/middleware/restRateLimit.test.ts src/routes/billing.ratelimit.test.ts --runInBand Blocked by existing pretest error-catalog drift in src/errors/codes.ts, docs/error-codes.md and docs/openapi.json.
node node_modules/jest/bin/jest.js src/middleware/restRateLimit.test.ts src/routes/billing.ratelimit.test.ts tests/integration/requireAuth.test.ts --runInBand --forceExit 64 passed, 2 failed. REST and authentication suites pass. Both billing failures reproduce unchanged on upstream: stale unsigned identity fixture and obsolete top-level error-code assertion.
node node_modules/jest/bin/jest.js src/middleware/restRateLimit.test.ts tests/integration/requireAuth.test.ts --runInBand --forceExit --silent Final rerun: 60 passed, both suites pass. HTTP quota clock is fixed to prevent elapsed execution time from making exact Retry-After assertions flaky.
node node_modules/jest/bin/jest.js src/middleware/restRateLimit.test.ts --runInBand --coverage --collectCoverageFrom=src/middleware/restRateLimit.ts --coverageReporters=text --forceExit All 31 tests passed; 97.29% statements, 93.54% branches, 97.05% lines in REST limiter.
node node_modules/typescript/bin/tsc --noEmit Existing workspace failures: 273 diagnostics, identical line-for-line on upstream and fix.
node node_modules/eslint/bin/eslint.js src/middleware/restRateLimit.ts src/middleware/restRateLimit.test.ts src/middleware/requireAuth.ts Blocked before linting by locked AJV incompatibility: missing ajv/lib/refs/json-schema-draft-04.json.
git diff --check Passed.

Workspace-wide build/CI cannot be certified while the missing manifest and baseline errors remain. This change does not alter those unrelated files or weaken validation.

Acceptance Criteria Status / Evidence
Changing x-user-id per request does not reset the limit HTTP regression rotates headers and gets 429 on request three; key tests ignore even valid signed forwarding. JWT subject regression also rotates legacy aliases and headers without resetting quota.
Bucket count stays bounded under a 100k-unique-key load Load regression checks the 10,000 bound throughout 100,000 insertions and verifies retained-key quotas. Optional smaller capacity exercises deterministic LRU eviction.
Expired buckets removed within one window Fake-clock test creates staggered idle buckets and verifies removal no later than one window after expiry, without new traffic; also verifies timer release when empty.
restRateLimit.test.ts covers eviction Dedicated eviction suite checks hard bound, LRU retention of denied active clients, expired peek deletion, reset/dispose/restart and invalid capacity.

@drips-wave

drips-wave Bot commented Sep 29, 2026

Copy link
Copy Markdown

@Erosenin2 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Evict idle buckets and key REST limits on verified identity

1 participant